<?php

namespace Desktopd\MUA;


// MultipartMessage (Raw MIME)
class MultipartMessage implements \IteratorAggregate, \ArrayAccess, \Countable {
    // multipart/signed messages are not supported by /this class/.
    const MULTIPART_MIXED = 1;
    const MULTIPART_ALTERNATIVE = 2;
    const MULTIPART_RELATED = 3;
    const MULTIPART_SIGNED = 4;
    
    // Content-Transfer-Encoding of this multipart message (as whole)
    const CONTENT_TRANSFER_ENCODING_7BIT = 1;
    const CONTENT_TRANSFER_ENCODING_8BIT = 2;
    const CONTENT_TRANSFER_ENCODING_BINARY = 3;
    
    
    protected $type = self::MULTIPART_MIXED;
    protected $parts; // \SplDoublyLinkedList
    
    protected $boundaryPrefix = '--_boundary_';
    protected $contentTransferEncoding = self::CONTENT_TRANSFER_ENCODING_7BIT;
    
    
    protected function pushPart (ReceivedMessage $part) {
        $this->parts->push($part);
    }
    
    public function __construct (Message $message = null, $strict = false) {
        $this->parts = new \SplDoublyLinkedList;
        
        try {
            if (!($message instanceof Message)) {
                throw new \RuntimeException('Not a Message');
            }
            
            $encoding = $message->searchHeader('Content-Transfer-Encoding');
            if (!$encoding) {
                // 7bit (default)
            } elseif ('8bit' === strtolower($encoding)) {
                $this->contentTransferEncoding = self::CONTENT_TRANSFER_ENCODING_8BIT;
            } elseif ('binary' === strtolower($encoding)) {
                $this->contentTransferEncoding = self::CONTENT_TRANSFER_ENCODING_BINARY;
            } else {
                // 7bit?
            }
            
            $type = $message->searchHeader('Content-Type');
            if (!preg_match('~^(multipart/.+?);\s*boundary="?(.+?)"?$~', $type, $matches)) {
                throw new \RuntimeException('Invalid content type');
            }
            
            list($matches, $type, $boundary) = $matches;
            //var_dump($type, $boundary);
            
            switch (strtolower($type)) {
                case 'signed':
                    $this->setType(self::MULTIPART_SIGNED);
                    break;
                
                case 'alternative':
                    $this->setType(self::MULTIPART_ALTERNATIVE);
                    break;
                
                case 'related':
                    $this->setType(self::MULTIPART_RELATED);
                    break;
                
                case 'mixed':
                default:
                    $this->setType(self::MULTIPART_MIXED);
                    break;
                
            }
            
            $lines = $message->getBodyLines();
            
            $inPart = false;
            $part = '';
            foreach ($lines as $line) {
                if ($line === "--$boundary") {
                    if ($inPart) {
                        $this->pushPart(new ReceivedMessage($part));
                        $part = '';
                    }
                    $inPart = true;
                    continue;
                } elseif ($line === "--$boundary--") {
                    if ($inPart) {
                        $this->pushPart(new ReceivedMessage($part));
                        $part = '';
                    }
                    $inPart = false;
                    break;
                }
                
                if ($inPart) {
                    $part .= "$line\r\n";
                }
            }
            
        } catch (\RuntimeException $e) {
            if ($strict) {
                throw $e;
            }
        }
    }
    
    public function setType ($type = null) {
        if ($type) {
            $this->type = $type;
        }
    }
    
    public function getType () {
        return $this->type;
    }
    
    public function getBoundaryPrefix () {
        return $this->boundaryPrefix;
    }
    
    public function setBoundaryPrefix ($prefix) {
        $prefix = "$prefix";
        if (!preg_match('~^[-_=a-zA-Z0-9+/.*&%$#@!^|:;,?<>()]+$~', $prefix)) {
            return false;
        }
        
        return $this->boundaryPrefix = $prefix;
    }
    
    public function addPart (Message $part) {
        $encoding = strtolower($part->searchHeader('Content-Transfer-Encoding'));
        if ('8bit' === $encoding) {
            $this->contentTransferEncoding = max(
                $this->contentTransferEncoding
                , self::CONTENT_TRANSFER_ENCODING_8BIT
            );
        } elseif ('binary' === $encoding) {
            $this->contentTransferEncoding = max(
                $this->contentTransferEncoding
                , self::CONTENT_TRANSFER_ENCODING_BINARY
            );
        } else {
            // assume 7bit (default)
        }
        
        $this->pushPart(new ReceivedMessage($part));
    }
    
    /**
        @returns NewMessage
    */
    public function toNewMessage () {
        //throw new \RuntimeException('Not implemented yet');
        
        
        switch ($this->type) {
            case self::MULTIPART_SIGNED:
                $contentType = 'multipart/signed';
                // TODO: not supported
                break;
            
            case self::MULTIPART_ALTERNATIVE:
                $contentType = 'multipart/alternative';
                break;
            
            case self::MULTIPART_RELATED:
                $contentType = 'multipart/related';
                break;
            
            case self::MULTIPART_MIXED:
            default:
                $contentType = 'multipart/mixed';
                break;
            
        }
        
        /* Slow, but no conflicts */
        $conflicts = array();
        $prefix = $this->getBoundaryPrefix();
        $partCount = 0;
        foreach ($this->parts as $part) {
            $partCount++;
            $matches = $part->searchPrefix("--$prefix");
            foreach ($matches as $line) {
                if (!in_array($line, $conflicts, true)) {
                    $conflicts[] = $line;
                }
            }
        }
        
        if ($partCount < 1) {
            throw new \RuntimeException('Empty multipart message!');
        }
        
        do {
            $boundary = $prefix . bin2hex(openssl_random_pseudo_bytes(16));
        } while (in_array("--$boundary", $conflicts) || in_array("--$boundary--", $conflicts));
        
        switch ($this->contentTransferEncoding) {
            case self::CONTENT_TRANSFER_ENCODING_8BIT:
                $encoding = '8bit';
                break;
            
            case self::CONTENT_TRANSFER_ENCODING_BINARY:
                $encoding = 'binary';
                break;
            
            default:
                $encoding = '7bit';
        }
        
        $body = 'This is a multi-part message in MIME format.';
        foreach ($this->parts as $part) {
            $body .= "\r\n--$boundary\r\n";
            $body .= $part->exportRaw();
        }
        
        $body .= "\r\n--$boundary--\r\n";
        
        $raw = <<<EOF
Content-Type: $contentType;
 boundary="$boundary"
Content-Transfer-Encoding: $encoding

$body
EOF;
        
        $new = new NewMessage();
        $new->importRaw($raw);
        
        return $new;
    }
    
    public function getIterator () {
        return $this->parts;
    }
    
    public function offsetExists ($offset) {
        return isset($this->parts[$offset]);
    }
    
    public function offsetGet ($offset) {
        return isset($this->parts[$offset]) ? $this->parts[$offset] : null;
    }
    
    public function offsetSet ($offset, $value) {
        // NOOP
    }
    
    public function offsetUnset ($offset) {
        if (!isset($this->parts[$offset])) {
            return;
        }
        
        unset($this->parts[$offset]);
    }
    
    public function count () {
        return count($this->parts);
    }
}


// vim: ts=4 et ai

